from bzrc import BZRC, Command, ShootCommand
import sys, math, time, optparse, random, pprint

attractMaxDist = 100
repulseMaxDist = 80
tangentMaxDist = 30
attractWeight = .1
repulseWeight = .3
tangentWeight = 50
minAttract = 10
maxVel = 25
coneWidth = math.pi / 8

class Agent(object):

	def __init__(self, bzrc):
		self.bzrc = bzrc
		self.constants = self.bzrc.get_constants()
		self.bases = self.bzrc.get_bases()
		self.myBase = [base for base in self.bases if base.color == self.constants['team']][0]
		self.commands = []
		self.state = []
		self.pfield = PField(self.bzrc)

	def tick(self, time_diff):
		'''Some time has passed; decide what to do next'''
		# Get information from the BZRC server
		global myTeamColor
		mytanks, othertanks, flags, shots = self.bzrc.get_lots_o_stuff()
		self.mytanks = mytanks
		self.othertanks = othertanks
		self.flags = flags
		self.shots = shots
		self.enemies = [tank for tank in othertanks if tank.color !=
				self.constants['team']]
		if len(self.state) == 0: #One-time initialization of the desired states
			self.state = ["SeekFlag" for tank in mytanks] # Unused right now

		self.pfield.update()


		# Reset my set of commands (we don't want to run old commands)
		self.commands = []

		# Decide what to do with each of my tanks
		for bot in mytanks:
			self.behave(bot, time_diff)
		

		# Send the commands to the server
		results = self.bzrc.do_commands(self.commands)

	def behave(self, bot, time_diff):
		if (bot.flag != '-'):
			ignoreFriends = True
			targetX, targetY = self.pfield.find_center(self.myBase.coords)
		else :
			targetX, targetY = self.closest_flag(bot)
			ignoreFriends = False
		(desiredVel, desiredAngle) = self.pfield.calc_field(bot.x, bot.y, targetX, targetY, ignoreFriends)
		delAngle = self.normalize_angle(desiredAngle - bot.angle)
		command = Command(bot.index, desiredVel, 2 * delAngle)
		self.check_los(bot)
		self.commands.append(command)

	def normalize_angle(self, angle):
		'''Make any angle be between +/- pi.'''
		angle -= 2 * math.pi * int (angle / (2 * math.pi))
		if angle <= -math.pi:
			angle += 2 * math.pi
		elif angle > math.pi:
			angle -= 2 * math.pi
		return angle
		
	def closest_flag(self, bot):
		closest = -1
		dist = ()
		for flag in self.flags:
			if flag.color == self.constants['team'] or flag.poss_color == self.constants['team']:
				continue
			newDist = math.hypot(flag.x - bot.x, flag.y - bot.y)
			if newDist < dist:
				closest = flag
				dist = newDist
		if closest != -1:
			return closest.x, closest.y
		for tank in self.othertanks: # No flags to chase; let's shed some blood
			newDist = math.hypot(tank.x - bot.x, tank.y - bot.y)
			if newDist < dist:
				closest = tank
				dist = newDist
		if closest != -1: # Can't imagine there being NO tanks on the field
			return closest.x, closest.y
		return 0,0 # Just wanter to the origin; maybe shoot something en route
		
	def check_los(self, bot):
		fire = False
		minDist = () # Infinity
		global coneWidth
		for tank in self.othertanks:
			angle = math.atan2(tank.x - bot.x, tank.y - bot.y)
			minDist = min(math.hypot(tank.x - bot.x, tank.y - bot.y), minDist)
			if self.normalize_angle(angle - bot.angle) < coneWidth:
				fire = True
				break
		if fire:
			for tank in self.mytanks: # Check for friendly fire
				if tank.index == bot.index:
					continue
				angle = math.atan2(tank.x - bot.x, tank.y - bot.y)
				ffDist = math.hypot(tank.x - bot.x, tank.y - bot.y)
				if angle - bot.angle < coneWidth and ffDist < minDist:
					fire = False
					break
		if fire: # Clear to shoot at enemy
			command = ShootCommand(bot.index)
			self.commands.append(command)

def main():
	# Process CLI arguments.
	parser = optparse.OptionParser(usage = "%prog [options]",
								   version = "%prog 0.1")
	parser.add_option("-o", "--host", type="string",dest="host",
			metavar="HOST",default='localhost',
			help="hostname for the server")
	parser.add_option("-p", "--port", type="int",dest="port",
			metavar="PORT",default=3907,
			help="port number for the server")

	(options, args) = parser.parse_args()

	# Connect.
	#bzrc = BZRC(options.host, int(options.port), debug=True)
	bzrc = BZRC(options.host, int(options.port))

	agent = Agent(bzrc)

	prev_time = time.time()

	# Run the agent
	try:
		while True:
			newTime = time.time()
			time_diff = newTime - prev_time
			agent.tick(time_diff)
			prev_time = newTime
	except KeyboardInterrupt:
		print "Exiting due to keyboard interrupt."
		bzrc.close()

class PField(object):
	
	def __init__(self, bzrc):
		self.bzrc = bzrc
		self.obstacles = bzrc.get_obstacles()
		
	# Helper function for adding vectors
	def add_vectors(self, vector1, vector2):
		# Get the x and y components
		x1 = vector1["magnitude"] * math.cos(vector1["angle"])
		y1 = vector1["magnitude"] * math.sin(vector1["angle"])
		x2 = vector2["magnitude"] * math.cos(vector2["angle"])
		y2 = vector2["magnitude"] * math.sin(vector2["angle"])

		xtot = x1 + x2
		ytot = y1 + y2
		newMag = math.sqrt(xtot * xtot + ytot * ytot)
		newAng = math.atan2(ytot, xtot)
		newVect = {"magnitude" : newMag, "angle" : newAng}
		return newVect
		
	# Calculates the total strength of the field at a given point.
	def calc_field(self, botX, botY, targetX, targetY, ignore_friends=False):
		otherTanks = self.otherTanks
		field = {"angle" : 0, "magnitude" : 0}

		
		field = self.add_vectors(field, self.attract_field(botX, botY, targetX, targetY))

		for tank in otherTanks:
			field = self.add_vectors(field, self.repulse_field(botX, botY, tank.x, tank.y))
		
		if not ignore_friends:
			for tank in self.myTanks:
				field = self.add_vectors(field, self.repulse_field(botX, botY, tank.x, tank.y))

		for obstacle in self.obstacles:
			field = self.add_vectors(field, self.tangential_field(botX, botY, obstacle))

		return self.normalize_vector(field)
				
	
	# Calculates a repulsive field given a point we want to avoid
	def repulse_field(self, botX, botY, startX, startY):
		distance = math.hypot(startX - botX, startY - botY)
		newMag = 0
		newAng = math.atan2((startY - botY), (startX - botX)) + math.pi
		
		if distance == 0:
			distance = 1

		if distance < repulseMaxDist:
			newMag = repulseWeight * repulseMaxDist / distance

		newVect = {"magnitude" : newMag, "angle" : newAng}

		return newVect
		
	
	# Calculates an attractive field given a point we want to reach
	def attract_field(self, botX, botY, startX, startY):
		distance = math.hypot(startX - botX, startY - botY)
		newMag = 0
		newAng = math.atan2((startY - botY), (startX - botX))
		
		if distance == 0:
			distance = 1

		if distance < attractMaxDist:
			newMag = attractWeight * distance
		else:
			newMag = attractWeight * attractMaxDist
			
		if newMag < minAttract:
			newMag = minAttract

		newVect = {"magnitude" : newMag, "angle" : newAng}

		return newVect
	
	# Calculates a tangential field given an obstacle we want to avoid
	def tangential_field(self, botX, botY, obstacle):
		(xcenter, ycenter) = self.find_center(obstacle)
		
		# So now we can calculate field strength
		distance = math.hypot(xcenter - botX, ycenter - botY)
		estRadius = math.hypot(obstacle[1][0] - obstacle[0][0], obstacle[1][1] - obstacle[0][1]) / 2
			
		if distance == 0:
			distance = 1
		
		if distance > estRadius:
			distance = distance - estRadius
			
		newMag = 0
		newAng = math.atan2((ycenter - botY), (xcenter - botX)) + math.pi

		if distance < tangentMaxDist:
			newMag = tangentWeight * tangentMaxDist / distance
			
		newAng = newAng - math.pi / 2 - random.uniform(0, 0.1) # I want the tangential field to be slightly repulsive, as well.

		newVect = {"magnitude" : newMag, "angle" : newAng}

		return newVect
		
	# Helper function for finding the center of an obstacle
	def find_center(self, obstacle):
		return (sum([pair[0] for pair in obstacle])/len(obstacle), sum([pair[1] for pair in obstacle])/len(obstacle))
				
	# Helper function for normalizing a vector by maxVel
	def normalize_vector(self, vect):
		return (vect["magnitude"] * 2, vect["angle"])
		
	def update(self):
		self.otherTanks = self.bzrc.get_othertanks()
		self.myTanks = self.bzrc.get_mytanks()


if __name__ == '__main__':
	main()

# vim: et sw=4 sts=4
